import { createImplementation } from "@webiny/di";
import { CliCommand, GetProjectSdkService, StdioService, UiService } from "~/abstractions/index.js";
import { DeployOutput } from "./deployOutputs/DeployOutput.js";
import { AppName } from "@webiny/project";
import { BuildRunner } from "~/features/BuildCommand/buildRunners/BuildRunner.js";
import { setTimeout } from "node:timers/promises";
import ora from "ora";
import open from "open";

// TODO: convert to a real service.
import { PrintInfoForEnv } from "~/features/InfoCommand/PrintInfoForEnv.js";

export interface IDeployCommandParams {
    apps?: AppName[];
    variant?: string;
    region?: string;
    env: string;
    deploymentLogs?: boolean;
    build?: boolean;
    preview?: boolean;
}

export interface IDeploySingleAppParams {
    app: AppName;
    variant?: string;
    region?: string;
    env: string;
    deploymentLogs?: boolean;
    build?: boolean;
    preview?: boolean;
}

const sleep = (ms: number = 1500) => setTimeout(ms);

export class DeployCommand implements CliCommand.Interface<IDeployCommandParams> {
    constructor(
        private getProjectSdkService: GetProjectSdkService.Interface,
        private uiService: UiService.Interface,
        private stdioService: StdioService.Interface
    ) {}

    async execute(): Promise<CliCommand.CommandDefinition<IDeployCommandParams>> {
        const projectSdk = await this.getProjectSdkService.execute();
        const ui = this.uiService;

        return {
            name: "deploy",
            description: "Deploys specified app or all apps in the project",
            examples: [
                "$0 deploy api --env dev",
                "$0 deploy core api --env dev",
                "$0 deploy admin --env prod",
                "$0 deploy --env prod",
                "$0 deploy"
            ],
            params: [
                {
                    name: "apps",
                    description:
                        "Name of the app(s) to deploy (core, admin, or api). You can specify multiple apps.",
                    type: "string",
                    array: true
                }
            ],
            options: [
                {
                    name: "env",
                    description: "Environment name (dev, prod, etc.)",
                    type: "string",
                    default: "dev",
                    validation: params => {
                        if (params.apps && params.apps.length > 0 && !params.env) {
                            throw new Error("Environment name is required when deploying an app.");
                        }
                        return true;
                    }
                },
                {
                    name: "variant",
                    description: "Variant of the app to deploy",
                    type: "string",
                    validation: params => {
                        const isValid = projectSdk.isValidVariantName(params.variant);
                        if (isValid.isErr()) {
                            throw isValid.error;
                        }
                        return true;
                    }
                },
                {
                    name: "region",
                    description: "Region to target",
                    type: "string",
                    validation: params => {
                        const isValid = projectSdk.isValidRegionName(params.region);
                        if (isValid.isErr()) {
                            throw isValid.error;
                        }
                        return true;
                    }
                },
                {
                    name: "build",
                    description: "Build packages before deploying",
                    type: "boolean",
                    default: true
                },
                {
                    name: "preview",
                    description: "Preview the deploy instead of actually performing it",
                    type: "boolean",
                    default: false
                },
                {
                    name: "deployment-logs",
                    description: "Print deployment logs (automatically enabled in CI environments)",
                    type: "boolean",
                    default: false
                }
            ],
            handler: async (params: IDeployCommandParams) => {
                if (params.apps && params.apps.length > 0) {
                    // Deploy specified apps
                    for (const appName of params.apps) {
                        const appParams: IDeploySingleAppParams = {
                            ...params,
                            app: appName
                        };

                        const app = await projectSdk.getApp(appName);
                        ui.info("Deploying %s app...", app.getDisplayName());
                        await this.deployApp(appParams);
                        ui.newLine();
                    }
                } else {
                    const isCi = projectSdk.isCi();
                    const coreStack = await projectSdk.getAppStackOutput({
                        app: "core",
                        env: params.env,
                        variant: params.variant
                    });

                    const isFirstDeployment = !isCi && !coreStack?.deploymentId;
                    if (isFirstDeployment) {
                        ui.info(`Looks like this is your first time deploying the project.`);
                        ui.info(
                            `Note that the initial deployment can take up to %s, so please be patient.`,
                            "10 minutes"
                        );
                        await sleep();
                    }

                    // 3. Start deploying apps one-by-one.
                    isFirstDeployment && ui.newLine();

                    // Deploy all apps in the project.
                    ui.info("Deploying %s app...", "Core");
                    await this.deployApp({ ...params, app: "core" });
                    ui.newLine();

                    ui.info("Deploying %s app...", "API");
                    await this.deployApp({ ...params, app: "api" });
                    ui.newLine();
                    ui.info("Deploying %s app...", "Admin");
                    await this.deployApp({ ...params, app: "admin" });

                    if (isFirstDeployment) {
                        ui.success(`Congratulations! You've just deployed a brand new project!`);
                    } else {
                        ui.success(`Project deployed.`);
                    }

                    const printInfoForEnv = new PrintInfoForEnv({
                        getProjectSdkService: this.getProjectSdkService,
                        uiService: this.uiService
                    });

                    ui.newLine();
                    ui.textBold("Project Details");
                    await printInfoForEnv.execute(params);

                    const adminAppOutput = await projectSdk.getAppStackOutput({
                        ...params,
                        app: "admin"
                    });

                    if (isFirstDeployment && adminAppOutput) {
                        ui.newLine();
                        ui.info(
                            "The final step is to open the %s app in your browser and complete the installation wizard.",
                            "Admin"
                        );

                        const spinner = ora(`Opening Admin in your browser...`).start();

                        try {
                            await sleep(7000);
                            open(adminAppOutput.appUrl as string);
                            spinner.succeed(`Successfully opened Admin app in your browser.`);
                        } catch {
                            spinner.fail(`Failed to open Admin in your browser.`);

                            await sleep(1000);
                            ui.newLine();
                            ui.warning(
                                `Failed to open %s app in your browser. To finish the setup and start using the project, please visit %s and complete the installation wizard.`,
                                "Admin",
                                adminAppOutput.appUrl
                            );
                        }
                    }
                }
            }
        };
    }

    private async deployApp(params: IDeploySingleAppParams) {
        const projectSdk = await this.getProjectSdkService.execute();

        const ui = this.uiService;
        const stdio = this.stdioService;

        if (params.build !== false) {
            const packagesBuilder = await projectSdk.buildApp(params);

            const buildRunner = new BuildRunner({
                stdio,
                ui,
                packagesBuilder
            });

            if (!buildRunner.isEmpty()) {
                await buildRunner.run();
                ui.newLine();
            }
        }

        // We always show deployment logs when doing previews.
        const showDeploymentLogs = Boolean(
            projectSdk.isCi() || params.preview || params.deploymentLogs
        );

        return await projectSdk.deployApp({
            ...params,
            output: pulumiProcess => {
                const deployOutput = new DeployOutput({
                    stdio,
                    ui,
                    showDeploymentLogs,
                    deployProcess: pulumiProcess,
                    deployParams: params
                });

                return deployOutput.output();
            }
        });
    }
}

export const deployCommand = createImplementation({
    abstraction: CliCommand,
    implementation: DeployCommand,
    dependencies: [GetProjectSdkService, UiService, StdioService]
});
